Conversation
WebKit on iOS does not show text being composed by an IME inside the terminal, so users type blind until it commits. Add mobile-ime-preview.js, a visual-only controller that renders the composition in the xterm helper layer and holds a committed chunk until local echo, parsed terminal output or a 2s fallback shows it. Wire it into terminal-ui.js, the script order, the build minify/hash lists and styles, with unit and wiring tests.
|
Thanks for this, @aakhter. This adds an in-terminal preview of IME composition text on iOS, so people typing Japanese, Chinese or Korean can see what they are composing before it commits, and it stays fully inert everywhere else. The wiring is careful: the output-sequence guard against pre-commit output, the split-chunk guard, the session-switch reset, the codex byte-identity and the build, A few things before merge: 1. The keydown finalization runs after xterm has already finalized ( 2. Bound the 3. A run on a real iPhone. Every line of this executes only on iOS WebKit, and the description notes it was driven with synthetic events in Chromium. xterm's own composition view ( 4. Smaller things I can also fold in at merge time if you prefer:
Once 1 to 4 are addressed and it has been seen working on an iPhone, this is ready to merge. |
- Observe keydown in the capture phase on terminal.element, an ancestor of the helper textarea, so the controller sees it before xterm's own capture listener finalizes the composition and emits the commit through onData. Finalize on exactly the keys CompositionHelper.keydown does (every keyCode except 20/229/16/17/18), ignoring isComposing and key as xterm does. - Bound awaitingCommit with the same 2 s fallback as the committed phase, so a composition whose commit never reaches onData cannot turn the next unrelated keystroke or paste into an IME commit. - pagehide resets the controller instead of destroying it, so a back-forward cache restore keeps the preview working. - Give the preview an opaque background from the terminal theme. - Route an IME commit through the ordinary printable/paste local echo branch and complete the commit afterwards; drop the send-on-throw fallback. - Pin the event order with an xterm stand-in registered in the capture phase ahead of the controller, and against real xterm in a browser test. - CLAUDE.md: note the IME commit routing and the z-index 6 preview layer.
|
Thanks for the careful review, and for reproducing item 1 against real xterm.
Small ones, all done: the preview takes the terminal theme background alongside the foreground; the commit now goes through the normal printable/paste branch with |
|
Thanks for the quick turnaround, @aakhter. This PR paints the text an IME is composing inside the terminal on iOS Safari, and the second commit covers everything from the first round: the capture-phase keydown on One thing needs a change before merge: Later compositions in a prompt are drawn under the local-echo overlay (
I reproduced it in Chromium with real xterm 6.0, the real overlay bundle and The ask: when the overlay has text, draw the composition after its pending text and above it. One option is a small addition to the overlay itself: a One small thing, fine to fold in with the fix, or I can do it at merge time:
Once that lands I will run it on my iPhone as promised: kana and pinyin, local echo on in a Claude session and off in a shell, and Return mid-composition. If that looks right, this is ready to merge. |
With local echo on, committed text sits in the LocalEchoOverlay and does not reach the PTY before Enter, so the PTY cursor that places the preview span stays at the prompt start. The span's z-index 6 only counts inside .xterm-helpers (its own z-index 5 stacking context), and the overlay is a z-index 7 layer whose first line is opaque from the prompt column, so every composition after the first one in a prompt was drawn under the overlay. - xterm-zerolag-input: add setComposition(text) and a composition getter. The overlay draws the composition as an underlined, aria-hidden tail after its pending text, through the same wrapping and grow-upward layout. It is never part of pendingText, hasPending or anything sent; clear() and removeChar() drop it, and rerender()/refreshFont() keep it. - terminal-ui.js: while local echo shows typed text (on, and not handed back to PTY echo by a nav key), render and clear the preview through setComposition. The helper span stays for local echo off, and as the fallback when the overlay cannot place the text (no prompt found). - Browser test against real xterm 6, the overlay bundled from its source and styles.css: a second composition after pending text is the topmost element after that text, and the commit lands in the overlay once. Unit tests for setComposition in the package and for the routing in the structure test. - CLAUDE.md and architecture-invariants: state the preview's effective layer.
|
Thanks, good catch, and thanks for the precise stacking analysis. Fixed in ee1a155, going with your first option.
Looking forward to your iPhone run. |
What
On iOS Safari, text being composed by an IME (Japanese, Chinese, Korean, and also the predictive/autocorrect composition on English keyboards) is not visible in the terminal until it commits, so users type blind. This adds
mobile-ime-preview.js, a visual-only controller that paints the in-progress composition in the xterm helper layer (at the same position as the helper textarea), and holds a committed chunk on screen until something authoritative shows it: the local-echo overlay, parsed terminal output that arrived after the commit, or a 2s fallback.How
src/web/public/mobile-ime-preview.js: the controller (MobileImePreview.create()/isIosWebKitTouch()). It only mounts on iOS WebKit touch devices; everywhere else_initMobileImePreview()returns early and no DOM is added.terminal-ui.js: init/destroy aroundterminal.open(), a reset on session switch, and two small hooks:handleTerminalData: if the chunk is the IME's commit and local echo is on, it goes to the overlay (which then displays it in place of the preview). If the overlay cannot take it, the text is sent directly rather than dropped.batchTerminalWrite/ flush: an output arrival counter, so the preview only clears on output that arrived after the commit and was fully parsed. Output queued before the commit, a split chunk, or another session's output never clears it early.terminal-keycode229-recovery.js: script tag, build minify step,HASHABLEentry (sosw.jsprecaches it), three CSS rules, and the CLAUDE.md load order.Every code path is wrapped so a preview failure can never block or drop input.
Testing
test/mobile-ime-preview.test.ts(34) covers the controller.test/mobile-ime-preview-structure.test.ts(31) covers the wiring, script order, build/hash registration, CSS and the terminal-ui.js hooks.HASHABLEentry each fails at least one test.tsc, lint,check:frontend-syntax,format:checkandcheck:public-assetsare clean.npm run buildemits the hashed asset, and bothindex.htmlandsw.jsreference it.Reviewer notes
compositionstart/compositionupdate/inputevents.